<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta content="" name="keywords">

  <title> Docker - </title>
  <meta name="description" content="" />
  <meta property="og:title" content="Docker" />
  <meta name="twitter:title" content="Docker" />
  <meta name="author" content="{Description { .Site.Author.name }}"/>
  <meta name="twitter:card" content="summary" />
  <meta property="og:url" content="https://systemime.github.io/categories/docker/" />
  <meta property="og:type" content="website" />
  <meta property="og:site_name" content="听风的小站" />

  <meta name="generator" content="Hugo 0.67.0" />
  <link rel="canonical" href="https://systemime.github.io/categories/docker/" />
  <link rel="alternate" href="https://systemime.github.io/index.xml" type="application/rss+xml" title="听风的小站">
  <script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/jquery-1.12.4.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.7.1/katex.min.css" integrity="sha384-wITovz90syo1dJWVh32uuETPVEtGigN07tkttEqPv+uR2SE/mbQcG7ATL28aI9H0" crossorigin="anonymous">
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  
  
  <link rel="stylesheet" href="https://systemime.github.io/css/main.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/my.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/pace.css" />
  <link rel="stylesheet" href="https://cdn.plyr.io/3.4.7/plyr.css">
  <link rel="stylesheet" href="https://systemime.github.io/css/lightgallery.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/baguetteBox.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/search.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/reward.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/share.min.css" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" />
  <link rel="stylesheet" href="https://systemime.github.io/css/pygment_highlights.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/highlight.min.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/toc.css" />
  <link rel="stylesheet" href="https://systemime.github.io/css/lightbox.css" />
  <link rel="stylesheet" href="https://systemime.github.io/live2d/css/live2d.css" />
  
  <link href="https://systemime.github.io/css/video-js.css" rel="stylesheet">


<meta name="baidu-site-verification" content="g8IYR9SNLF" />
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://hm.baidu.com/hm.js?cffe439e37449bb1c07ab26ab56484bb";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script>

<link rel="stylesheet" href="https://systemime.github.io/css/prism.css" />









<script src="https://systemime.github.io/js/pace.min.js" data-no-instant></script>



<script src="https://systemime.github.io/js/instantclick.min.js" data-no-instant></script>
   <script data-no-instant>
   InstantClick.on('change', function(isInitialLoad) {
     if (isInitialLoad === false) {
       if (typeof MathJax !== 'undefined') 
         MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
       if (typeof prettyPrint !== 'undefined') 
         prettyPrint();
       if (typeof _hmt !== 'undefined')  
         _hmt.push(['_trackPageview', location.pathname + location.search]);
       if (typeof ga !== 'undefined')  
           ga('send', 'pageview', location.pathname + location.search);
     }
   });
   InstantClick.init();
</script>




<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.7.1/gist-embed.min.js"></script>


<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
  (adsbygoogle = window.adsbygoogle || []).push({
    google_ad_client: "ca-pub-3925981084585036",
    enable_page_level_ads: true
  });
</script>



<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/photoswipe.min.css" integrity="sha256-sCl5PUOGMLfFYctzDW3MtRib0ctyUvI9Qsmq2wXOeBY=" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.1/default-skin/default-skin.min.css" integrity="sha256-BFeI1V+Vh1Rk37wswuOYn5lsTcaU96hGaI7OUVCLjPc=" crossorigin="anonymous" />



<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">

<div class="pswp__bg"></div>

<div class="pswp__scroll-wrap">
    
    <div class="pswp__container">
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
      <div class="pswp__item"></div>
    </div>
    
    <div class="pswp__ui pswp__ui--hidden">
    <div class="pswp__top-bar">
      
      <div class="pswp__counter"></div>
      <button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
      <button class="pswp__button pswp__button--share" title="Share"></button>
      <button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
      <button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
      
      
      <div class="pswp__preloader">
        <div class="pswp__preloader__icn">
          <div class="pswp__preloader__cut">
            <div class="pswp__preloader__donut"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
      <div class="pswp__share-tooltip"></div>
    </div>
    <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)">
    </button>
    <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)">
    </button>
    <div class="pswp__caption">
      <div class="pswp__caption__center"></div>
    </div>
    </div>
    </div>
</div>

</head>

  <body>
    <nav class="navbar navbar-default navbar-fixed-top navbar-custom">
  <div class="container-fluid">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#main-navbar">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="https://systemime.github.io">听风的小站</a>
    </div>
    <div class="collapse navbar-collapse" id="main-navbar">
      <ul class="nav navbar-nav navbar-right">
        
          
            <li>
		    <a title="博客" href="https://systemime.github.io/blog/">博客</a>
              
              
            </li>
          
        
          
            <li>
		    <a title="片刻" href="https://systemime.github.io/moment/">片刻</a>
              
              
            </li>
          
        
          
            <li>
		    <a title="关于" href="https://systemime.github.io/about/">关于</a>
              
              
            </li>
          
        
          
            <li>
		    <a title="留言" href="https://systemime.github.io/message/">留言</a>
              
              
            </li>
          
        

        

        
        
      </ul>
    </div>

    <div class="avatar-container">
      <div class="avatar-img-border">
        
      </div>
    </div>

  </div>
</nav>






    
  <header class="header-section ">
    <div class="intro-header no-img">
      <div class="container">
        <div class="row">
          <div class="col-lg-12 col-md-12 col-md-offset-0">
            <div class="page-heading">
              <h1>#Docker</h1>
              <hr class="small">
            </div>
          </div>
        </div>
      </div>
    </div>
  </header>

    
  <div class="container" role="main">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        
        <div class="posts-list">
          
            <article class="post-preview">
              <a href="https://systemime.github.io/blog/2019-02/docker%E6%93%8D%E4%BD%9C%E5%91%BD%E4%BB%A4%E5%9F%BA%E7%A1%80%E7%AF%87/">
                <h2 class="post-title">Docker操作命令——基础篇</h2>

                
              </a>

              <p class="post-meta">
                Posted on February 19, 2019
              </p>
              <div class="post-entry">
                
                  <h2 id="基本命令">基本命令</h2>
<h4 id="docker基本状态转换和容器相关图">docker基本状态转换和容器相关图</h4>
<p><img src="https://github.com/systemime/my_image/blob/master/Docker%E6%93%8D%E4%BD%9C%E5%91%BD%E4%BB%A4%E2%80%94%E2%80%94%E5%9F%BA%E6%9C%AC%E7%AF%87/%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%92%8C%E5%91%BD%E4%BB%A4%E5%9B%BE.jpg?raw=true" alt="基本状态转换和容器相关图"></p>
<h4 id="拉取docker镜像">拉取docker镜像</h4>
<pre><code>docker pull image_name
</code></pre><h4 id="查看宿主机上的镜像docker镜像保存在varlibdocker目录下">查看宿主机上的镜像，Docker镜像保存在/var/lib/docker目录下:</h4>
<pre><code>docker images
</code></pre><h4 id="删除镜像">删除镜像</h4>
<pre><code>docker rmi docker.io/tomcat:7.0.77-jre7 或者 docker rmi b39c68b7af30
</code></pre><h4 id="查看当前有哪些容器正在运行">查看当前有哪些容器正在运行</h4>
<pre><code>docker ps
</code></pre><h4 id="查看所有容器">查看所有容器</h4>
<pre><code>docker ps -a
</code></pre><h4 id="运行docker镜像命令">运行Docker镜像命令</h4>
<pre><code># 交互式启动
docker run -it --rm ubuntu:18.04 bash

# 进入已经启动的容器内部
docker attach container_name/container_id

# 推荐的，进入已经启动的容器内部方法
docker exec -t -i CONTAINER_ID /bin/bash
</code></pre><p><code>docker run</code> 就是运行容器的命令，具体格式我们会在 <a href="../container">容器</a> 一节进行详细讲解，我们这里简要的说明一下上面用到的参数。</p>
<ul>
<li><code>-it</code>：这是两个参数，一个是 <code>-i</code>：交互式操作，一个是 <code>-t</code> 终端。我们这里打算进入 <code>bash</code> 执行一些命令并查看返回结果，因此我们需要交互式终端。</li>
<li><code>--rm</code>：这个参数是说容器退出后随之将其删除。默认情况下，为了排障需求，退出的容器并不会立即删除，除非手动 <code>docker rm</code>。我们这里只是随便执行个命令，看看结果，不需要排障和保留结果，因此使用 <code>--rm</code> 可以避免浪费空间。</li>
<li><code>ubuntu:18.04</code>：这是指用 <code>ubuntu:18.04</code> 镜像为基础来启动容器。</li>
<li><code>bash</code>：放在镜像名后的是<strong>命令</strong>，这里我们希望有个交互式 Shell，因此用的是 <code>bash</code>。</li>
</ul>
<p>进入容器后，我们可以在 Shell 下操作，执行任何所需的命令。这里，我们执行了 <code>cat /etc/os-release</code>，这是 Linux 常用的查看当前系统版本的命令，从返回的结果可以看到容器内是 <code>Ubuntu 18.04.1 LTS</code> 系统。</p>
<p>最后我们通过 <code>exit</code> 退出了这个容器。</p>
<p>** 若想要退出容器而不停止容器运行，请参阅本站《Docker——退出而不关闭容器》一文 **</p>
<h4 id="查看当前系统docker信息">查看当前系统Docker信息</h4>
<pre><code>docker info
</code></pre><h4 id="获取容器日志">获取容器日志</h4>
<pre><code>docker containers logs docker-name
# 或
docker logs docker-name
</code></pre>
                
              </div>

              
                <span class="post-meta">
                  
                    #<a href="https://systemime.github.iotags/docker/">Docker</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/%E5%9F%BA%E7%A1%80/">基础</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/%E6%93%8D%E4%BD%9C/">操作</a>&nbsp;
                  
                </span>
              

            </article>
          
            <article class="post-preview">
              <a href="https://systemime.github.io/blog/2019-02/docker%E9%80%80%E5%87%BA%E8%80%8C%E4%B8%8D%E5%85%B3%E9%97%AD%E5%AE%B9%E5%99%A8/">
                <h2 class="post-title">Docker——退出而不关闭容器</h2>

                
              </a>

              <p class="post-meta">
                Posted on February 19, 2019
              </p>
              <div class="post-entry">
                
                  <p>一般情况下，进入docker容器后如果退出容器，容器就会变成Exited的状态，那么如何退出容器让容器不关闭呢？</p>
<p>如果要正常退出不关闭容器，请按Ctrl+P+Q进行退出容器，这一点很重要，请牢记！</p>
<p>以下示例为退出容器但不关闭容器</p>
<pre><code>[root@localhost ~]# docker attach c600c4519fc8
[root@c600c4519fc8 /]# exit
exit
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
c600c4519fc8        centos              &quot;/bin/bash&quot;         3 hours ago         Exited (0) 1 second ago                       pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                20 hours ago        Exited (0) 20 hours ago                       hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;            46 hours ago        Exited (0) 46 hours ago                       hopeful_yonath
[root@localhost ~]# docker start pensive_jackson
pensive_jackson
[root@localhost ~]# docker attach c600c4519fc8

Ctrl + P + Q 

[root@c600c4519fc8 /]# read escape sequence
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
c600c4519fc8        centos              &quot;/bin/bash&quot;         3 hours ago         Up 22 seconds                                 pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                20 hours ago        Exited (0) 20 hours ago                       hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;            46 hours ago        Exited (0) 46 hours ago                       hopeful_yonath
</code></pre><p>事实上我们可以在启动容器的时候就进行配置，加入-d参数来启动容器，当然，这条命令仅限于启动全新的容器，启动关闭的容器是不可以的。</p>
<blockquote>
<p>Tips 1
docker run -d: 后台运行容器，并返回容器ID</p>
</blockquote>
<p>以下示例为使用docker -d启动容器并退出</p>
<pre><code>[root@localhost ~]# docker run -i -t -d centos /bin/bash
8521b11d5d99535d4cb0080adc5a58a4dd018ecd0751d9945f7da7ab01bec330
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
8521b11d5d99        centos              &quot;/bin/bash&quot;         4 seconds ago       Up 4 seconds                                    eager_goldwasser
c600c4519fc8        centos              &quot;/bin/bash&quot;         3 hours ago         Exited (0) 28 seconds ago                       pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                20 hours ago        Exited (0) 20 hours ago                         hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;            46 hours ago        Exited (0) 46 hours ago                         hopeful_yonath
[root@localhost ~]# docker attach 8
[root@8521b11d5d99 /]# uname -r
3.10.0-514.el7.x86_64
[root@8521b11d5d99 /]# exit
exit
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
8521b11d5d99        centos              &quot;/bin/bash&quot;         2 minutes ago       Exited (0) 2 seconds ago                       eager_goldwasser
c600c4519fc8        centos              &quot;/bin/bash&quot;         3 hours ago         Exited (0) 2 minutes ago                       pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                20 hours ago        Exited (0) 20 hours ago                        hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;            46 hours ago        Exited (0) 46 hours ago                        hopeful_yonath
</code></pre><p>在这里你可能会发现，使用了-d的命令退出后容器依然还是死了，动手型的朋友可能会发现只是用docker run -d去启动容器也一样是死的
这里其实需要了解的是容器的运行机制，Docker容器在后台运行，必须要有一个前台进程，这里我们让容器有前台程序运行，就可以实现容器的-d 启动后存活</p>
<pre><code>[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
c600c4519fc8        centos              &quot;/bin/bash&quot;         3 hours ago         Exited (0) 4 minutes ago                       pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                21 hours ago        Exited (0) 21 hours ago                        hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;            47 hours ago        Exited (0) 47 hours ago                        hopeful_yonath
[root@localhost ~]# docker run -d centos /bin/bash -c &quot;nohup ping -i 1000 www.baidu.com&quot;
8aa19c9604382bc019797ccda831ae1bcebd81d86380b6040d636e03000b440a
[root@localhost ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
8aa19c960438        centos              &quot;/bin/bash -c 'nohup…&quot;   2 seconds ago       Up 2 seconds                                   adoring_wing
c600c4519fc8        centos              &quot;/bin/bash&quot;              3 hours ago         Exited (0) 5 minutes ago                       pensive_jackson
5a7a0d694651        busybox             &quot;sh&quot;                     21 hours ago        Exited (0) 21 hours ago                        hungry_vaughan
4b0296d18849        hello-world         &quot;/hello&quot;                 47 hours ago        Exited (0) 47 hours ago                        hopeful_yonath
</code></pre><p>我这里使用nohup在后台运行一个每1000秒ping一次百度的进程，另外你也可以使用&quot;while true; do echo hello world; sleep 1; done&rdquo;，无限输出hello world。</p>
<p>另外即便是有进程在后台运行，你进入了容器，输入exit退出，依然会终止容器的运行，请谨记。</p>
<p><strong>Ctrl+P+Q依然是我认为的最佳用法。</strong></p>

                
              </div>

              
                <span class="post-meta">
                  
                    #<a href="https://systemime.github.iotags/docker/">Docker</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/%E9%80%80%E8%80%8C%E4%B8%8D%E5%85%B3/">退而不关</a>&nbsp;
                  
                </span>
              

            </article>
          
            <article class="post-preview">
              <a href="https://systemime.github.io/blog/2019-02/dockerfile-%E5%A4%9A%E9%98%B6%E6%AE%B5%E6%9E%84%E5%BB%BA/">
                <h2 class="post-title">Dockerfile 多阶段构建</h2>

                
              </a>

              <p class="post-meta">
                Posted on February 14, 2019
              </p>
              <div class="post-entry">
                
                  <h2 id="多阶段构建">多阶段构建</h2>
<h3 id="之前的做法">之前的做法</h3>
<p>在 Docker 17.05 版本之前，我们构建 Docker 镜像时，通常会采用两种方式：</p>
<h4 id="全部放入一个-dockerfile">全部放入一个 Dockerfile</h4>
<p>一种方式是将所有的构建过程编包含在一个 <code>Dockerfile</code> 中，包括项目及其依赖库的编译、测试、打包等流程，这里可能会带来的一些问题：</p>
<ul>
<li>
<p>镜像层次多，镜像体积较大，部署时间变长</p>
</li>
<li>
<p>源代码存在泄露的风险</p>
</li>
</ul>
<p>例如，编写 <code>app.go</code> 文件，该程序输出 <code>Hello World!</code></p>
<div class="highlight"><pre class="chroma"><code class="language-go" data-lang="go"><span class="kn">package</span> <span class="nx">main</span>  

<span class="kn">import</span> <span class="s">&#34;fmt&#34;</span>  

<span class="kd">func</span> <span class="nf">main</span><span class="p">(){</span>  
    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Hello World!&#34;</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div><p>编写 <code>Dockerfile.one</code> 文件</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> golang:1.9-alpine</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apk --no-cache add git ca-certificates<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /go/src/github.com/go/helloworld/</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> app.go .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> go get -d -v github.com/go-sql-driver/mysql <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> <span class="nv">GOOS</span><span class="o">=</span>linux go build -a -installsuffix cgo -o app . <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> cp /go/src/github.com/go/helloworld/app /root<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /root/</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./app&#34;</span><span class="p">]</span><span class="err">
</span></code></pre></div><p>构建镜像</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker build -t go/helloworld:1 -f Dockerfile.one .
</code></pre></div><h4 id="分散到多个-dockerfile">分散到多个 Dockerfile</h4>
<p>另一种方式，就是我们事先在一个 <code>Dockerfile</code> 将项目及其依赖库编译测试打包好后，再将其拷贝到运行环境中，这种方式需要我们编写两个 <code>Dockerfile</code> 和一些编译脚本才能将其两个阶段自动整合起来，这种方式虽然可以很好地规避第一种方式存在的风险，但明显部署过程较复杂。</p>
<p>例如，编写 <code>Dockerfile.build</code> 文件</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> golang:1.9-alpine</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apk --no-cache add git<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /go/src/github.com/go/helloworld</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> app.go .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> go get -d -v github.com/go-sql-driver/mysql <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> <span class="nv">GOOS</span><span class="o">=</span>linux go build -a -installsuffix cgo -o app .<span class="err">
</span></code></pre></div><p>编写 <code>Dockerfile.copy</code> 文件</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> alpine:latest</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apk --no-cache add ca-certificates<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /root/</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> app .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./app&#34;</span><span class="p">]</span><span class="err">
</span></code></pre></div><p>新建 <code>build.sh</code></p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="cp">#!/bin/sh
</span><span class="cp"></span><span class="nb">echo</span> Building go/helloworld:build

docker build -t go/helloworld:build . -f Dockerfile.build

docker create --name extract go/helloworld:build
docker cp extract:/go/src/github.com/go/helloworld/app ./app
docker rm -f extract

<span class="nb">echo</span> Building go/helloworld:2

docker build --no-cache -t go/helloworld:2 . -f Dockerfile.copy
rm ./app
</code></pre></div><p>现在运行脚本即可构建镜像</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ chmod +x build.sh

$ ./build.sh
</code></pre></div><p>对比两种方式生成的镜像大小</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker image ls

REPOSITORY      TAG    IMAGE ID        CREATED         SIZE
go/helloworld   <span class="m">2</span>      f7cf3465432c    <span class="m">22</span> seconds ago  6.47MB
go/helloworld   <span class="m">1</span>      f55d3e16affc    <span class="m">2</span> minutes ago   295MB
</code></pre></div><h3 id="使用多阶段构建">使用多阶段构建</h3>
<p>为解决以上问题，Docker v17.05 开始支持多阶段构建 (<code>multistage builds</code>)。使用多阶段构建我们就可以很容易解决前面提到的问题，并且只需要编写一个 <code>Dockerfile</code>：</p>
<p>例如，编写 <code>Dockerfile</code> 文件</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> golang:1.9-alpine as builder</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apk --no-cache add git<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /go/src/github.com/go/helloworld/</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> go get -d -v github.com/go-sql-driver/mysql<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> app.go .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> <span class="nv">GOOS</span><span class="o">=</span>linux go build -a -installsuffix cgo -o app .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">FROM</span><span class="s"> alpine:latest as prod</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apk --no-cache add ca-certificates<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /root/</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> --from<span class="o">=</span><span class="m">0</span> /go/src/github.com/go/helloworld/app .<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./app&#34;</span><span class="p">]</span>  <span class="err">
</span></code></pre></div><p>构建镜像</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker build -t go/helloworld:3 .
</code></pre></div><p>对比三个镜像大小</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker image ls

REPOSITORY        TAG   IMAGE ID         CREATED            SIZE
go/helloworld     <span class="m">3</span>     d6911ed9c846     <span class="m">7</span> seconds ago      6.47MB
go/helloworld     <span class="m">2</span>     f7cf3465432c     <span class="m">22</span> seconds ago     6.47MB
go/helloworld     <span class="m">1</span>     f55d3e16affc     <span class="m">2</span> minutes ago      295MB
</code></pre></div><p>很明显使用多阶段构建的镜像体积小，同时也完美解决了上边提到的问题。</p>
<h4 id="只构建某一阶段的镜像">只构建某一阶段的镜像</h4>
<p>我们可以使用 <code>as</code> 来为某一阶段命名，例如</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="k">FROM</span><span class="s"> golang:1.9-alpine as builder</span><span class="err">
</span></code></pre></div><p>例如当我们只想构建 <code>builder</code> 阶段的镜像时，增加 <code>--target=builder</code> 参数即可</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker build --target builder -t username/imagename:tag .
</code></pre></div><h4 id="构建时从其他镜像复制文件">构建时从其他镜像复制文件</h4>
<p>上面例子中我们使用 <code>COPY --from=0 /go/src/github.com/go/helloworld/app .</code> 从上一阶段的镜像中复制文件，我们也可以复制任意镜像中的文件。</p>
<div class="highlight"><pre class="chroma"><code class="language-dockerfile" data-lang="dockerfile">$ COPY --from<span class="o">=</span>nginx:latest /etc/nginx/nginx.conf /nginx.conf<span class="err">
</span></code></pre></div>
                
              </div>

              
                <span class="post-meta">
                  
                    #<a href="https://systemime.github.iotags/docker/">Docker</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/dockerfile/">Dockerfile</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/%E5%A4%9A%E9%98%B6%E6%AE%B5%E6%9E%84%E5%BB%BA/">多阶段构建</a>&nbsp;
                  
                </span>
              

            </article>
          
            <article class="post-preview">
              <a href="https://systemime.github.io/blog/2019-02/dockerfile-%E6%8C%87%E4%BB%A4%E8%AF%A6%E8%A7%A3/">
                <h2 class="post-title">Dockerfile 指令详解</h2>

                
              </a>

              <p class="post-meta">
                Posted on February 14, 2019
              </p>
              <div class="post-entry">
                
                  <blockquote>
<p>本文简述了：COPY,ADD,CMD,ENTRYPOINT等命令介绍和使用方法</p>
</blockquote>
<h3 id="copy-复制文件">COPY 复制文件</h3>
<p>格式：</p>
<ul>
<li><code>COPY [--chown=&lt;user&gt;:&lt;group&gt;] &lt;源路径&gt;... &lt;目标路径&gt;</code></li>
<li><code>COPY [--chown=&lt;user&gt;:&lt;group&gt;] [&quot;&lt;源路径1&gt;&quot;,... &quot;&lt;目标路径&gt;&quot;]</code></li>
</ul>
<p>和 <code>RUN</code> 指令一样，也有两种格式，一种类似于命令行，一种类似于函数调用。</p>
<p><code>COPY</code> 指令将从构建上下文目录中 <code>&lt;源路径&gt;</code> 的文件/目录复制到新的一层的镜像内的 <code>&lt;目标路径&gt;</code> 位置。比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">COPY</span> package.json /usr/src/app/<span class="err">
</span></code></pre></div><p><code>&lt;源路径&gt;</code> 可以是多个，甚至可以是通配符，其通配符规则要满足 Go 的 <a href="https://golang.org/pkg/path/filepath/#Match"><code>filepath.Match</code></a> 规则，如：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">COPY</span> hom* /mydir/<span class="err">
</span><span class="err"></span><span class="k">COPY</span> hom?.txt /mydir/<span class="err">
</span></code></pre></div><p><code>&lt;目标路径&gt;</code> 可以是容器内的绝对路径，也可以是相对于工作目录的相对路径（工作目录可以用 <code>WORKDIR</code> 指令来指定）。目标路径不需要事先创建，如果目录不存在会在复制文件前先行创建缺失目录。</p>
<p>此外，还需要注意一点，使用 <code>COPY</code> 指令，源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。</p>
<p>在使用该指令的时候还可以加上 <code>--chown=&lt;user&gt;:&lt;group&gt;</code> 选项来改变文件的所属用户及所属组。</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">COPY</span> --chown<span class="o">=</span>55:mygroup files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">COPY</span> --chown<span class="o">=</span>bin files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">COPY</span> --chown<span class="o">=</span><span class="m">1</span> files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">COPY</span> --chown<span class="o">=</span>10:11 files* /mydir/<span class="err">
</span></code></pre></div><h3 id="add-更高级的复制文件">ADD 更高级的复制文件</h3>
<p><code>ADD</code> 指令和 <code>COPY</code> 的格式和性质基本一致。但是在 <code>COPY</code> 基础上增加了一些功能。</p>
<p>比如 <code>&lt;源路径&gt;</code> 可以是一个 <code>URL</code>，这种情况下，Docker 引擎会试图去下载这个链接的文件放到 <code>&lt;目标路径&gt;</code> 去。下载后的文件权限自动设置为 <code>600</code>，如果这并不是想要的权限，那么还需要增加额外的一层 <code>RUN</code> 进行权限调整，另外，如果下载的是个压缩包，需要解压缩，也一样还需要额外的一层 <code>RUN</code> 指令进行解压缩。所以不如直接使用 <code>RUN</code> 指令，然后使用 <code>wget</code> 或者 <code>curl</code> 工具下载，处理权限、解压缩、然后清理无用文件更合理。因此，这个功能其实并不实用，而且不推荐使用。</p>
<p>如果 <code>&lt;源路径&gt;</code> 为一个 <code>tar</code> 压缩文件的话，压缩格式为 <code>gzip</code>, <code>bzip2</code> 以及 <code>xz</code> 的情况下，<code>ADD</code> 指令将会自动解压缩这个压缩文件到 <code>&lt;目标路径&gt;</code> 去。</p>
<p>在某些情况下，这个自动解压缩的功能非常有用，比如官方镜像 <code>ubuntu</code> 中：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> scratch</span><span class="err">
</span><span class="err"></span><span class="k">ADD</span> ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /<span class="err">
</span><span class="err"></span>...<span class="err">
</span></code></pre></div><p>但在某些情况下，如果我们真的是希望复制个压缩文件进去，而不解压缩，这时就不可以使用 <code>ADD</code> 命令了。</p>
<p>在 Docker 官方的 <a href="https://tuxknight-notes.readthedocs.io/en/latest/docker/dockerfile_best_practices_take.html">Dockerfile 最佳实践文档</a> 中要求，尽可能的使用 <code>COPY</code>，因为 <code>COPY</code> 的语义很明确，就是复制文件而已，而 <code>ADD</code> 则包含了更复杂的功能，其行为也不一定很清晰。最适合使用 <code>ADD</code> 的场合，就是所提及的需要自动解压缩的场合。</p>
<p>另外需要注意的是，<code>ADD</code> 指令会令镜像构建缓存失效，从而可能会令镜像构建变得比较缓慢。</p>
<p>因此在 <code>COPY</code> 和 <code>ADD</code> 指令中选择的时候，可以遵循这样的原则，所有的文件复制均使用 <code>COPY</code> 指令，仅在需要自动解压缩的场合使用 <code>ADD</code>。</p>
<p>在使用该指令的时候还可以加上 <code>--chown=&lt;user&gt;:&lt;group&gt;</code> 选项来改变文件的所属用户及所属组。</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">ADD</span> --chown<span class="o">=</span>55:mygroup files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">ADD</span> --chown<span class="o">=</span>bin files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">ADD</span> --chown<span class="o">=</span><span class="m">1</span> files* /mydir/<span class="err">
</span><span class="err"></span><span class="k">ADD</span> --chown<span class="o">=</span>10:11 files* /mydir/<span class="err">
</span></code></pre></div><h3 id="cmd-容器启动命令">CMD 容器启动命令</h3>
<p><code>CMD</code> 指令的格式和 <code>RUN</code> 相似，也是两种格式：</p>
<ul>
<li><code>shell</code> 格式：<code>CMD &lt;命令&gt;</code></li>
<li><code>exec</code> 格式：<code>CMD [&quot;可执行文件&quot;, &quot;参数1&quot;, &quot;参数2&quot;...]</code></li>
<li>参数列表格式：<code>CMD [&quot;参数1&quot;, &quot;参数2&quot;...]</code>。在指定了 <code>ENTRYPOINT</code> 指令后，用 <code>CMD</code> 指定具体的参数。</li>
</ul>
<p>之前介绍容器的时候曾经说过，Docker 不是虚拟机，容器就是进程。既然是进程，那么在启动容器的时候，需要指定所运行的程序及参数。<code>CMD</code> 指令就是用于指定默认的容器主进程的启动命令的。</p>
<p>在运行时可以指定新的命令来替代镜像设置中的这个默认命令，比如，<code>ubuntu</code> 镜像默认的 <code>CMD</code> 是 <code>/bin/bash</code>，如果我们直接 <code>docker run -it ubuntu</code> 的话，会直接进入 <code>bash</code>。我们也可以在运行时指定运行别的命令，如 <code>docker run -it ubuntu cat /etc/os-release</code>。这就是用 <code>cat /etc/os-release</code> 命令替换了默认的 <code>/bin/bash</code> 命令了，输出了系统版本信息。</p>
<p>在指令格式上，一般推荐使用 <code>exec</code> 格式，这类格式在解析时会被解析为 JSON 数组，因此一定要使用双引号 <code>&quot;</code>，而不要使用单引号。</p>
<p>如果使用 <code>shell</code> 格式的话，实际的命令会被包装为 <code>sh -c</code> 的参数的形式进行执行。比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">CMD</span> <span class="nb">echo</span> <span class="nv">$HOME</span><span class="err">
</span></code></pre></div><p>在实际执行中，会将其变更为：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;sh&#34;</span><span class="p">,</span> <span class="s2">&#34;-c&#34;</span><span class="p">,</span> <span class="s2">&#34;echo $HOME&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>这就是为什么我们可以使用环境变量的原因，因为这些环境变量会被 shell 进行解析处理。</p>
<p>提到 <code>CMD</code> 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。</p>
<p>Docker 不是虚拟机，容器中的应用都应该以前台执行，而不是像虚拟机、物理机里面那样，用 upstart/systemd 去启动后台服务，容器内没有后台服务的概念。</p>
<p>一些初学者将 <code>CMD</code> 写为：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">CMD</span> service nginx start<span class="err">
</span></code></pre></div><p>然后发现容器执行后就立即退出了。甚至在容器内去使用 <code>systemctl</code> 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念，没有区分容器和虚拟机的差异，依旧在以传统虚拟机的角度去理解容器。</p>
<p>对于容器而言，其启动程序就是容器应用进程，容器就是为了主进程而存在的，主进程退出，容器就失去了存在的意义，从而退出，其它辅助进程不是它需要关心的东西。</p>
<p>而使用 <code>service nginx start</code> 命令，则是希望 upstart 来以后台守护进程形式启动 <code>nginx</code> 服务。而刚才说了 <code>CMD service nginx start</code> 会被理解为 <code>CMD [ &quot;sh&quot;, &quot;-c&quot;, &quot;service nginx start&quot;]</code>，因此主进程实际上是 <code>sh</code>。那么当 <code>service nginx start</code> 命令结束后，<code>sh</code> 也就结束了，<code>sh</code> 作为主进程退出了，自然就会令容器退出。</p>
<p>正确的做法是直接执行 <code>nginx</code> 可执行文件，并且要求以前台形式运行。比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;nginx&#34;</span><span class="p">,</span> <span class="s2">&#34;-g&#34;</span><span class="p">,</span> <span class="s2">&#34;daemon off;&#34;</span><span class="p">]</span><span class="err">
</span></code></pre></div><h3 id="entrypoint-入口点">ENTRYPOINT 入口点</h3>
<p><code>ENTRYPOINT</code> 的格式和 <code>RUN</code> 指令格式一样，分为 <code>exec</code> 格式和 <code>shell</code> 格式。</p>
<p><code>ENTRYPOINT</code> 的目的和 <code>CMD</code> 一样，都是在指定容器启动程序及参数。<code>ENTRYPOINT</code> 在运行时也可以替代，不过比 <code>CMD</code> 要略显繁琐，需要通过 <code>docker run</code> 的参数 <code>--entrypoint</code> 来指定。</p>
<p>当指定了 <code>ENTRYPOINT</code> 后，<code>CMD</code> 的含义就发生了改变，不再是直接的运行其命令，而是将 <code>CMD</code> 的内容作为参数传给 <code>ENTRYPOINT</code> 指令，换句话说实际执行时，将变为：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">&lt;ENTRYPOINT&gt; <span class="s2">&#34;&lt;CMD&gt;&#34;</span>
</code></pre></div><p>那么有了 <code>CMD</code> 后，为什么还要有 <code>ENTRYPOINT</code> 呢？这种 <code>&lt;ENTRYPOINT&gt; &quot;&lt;CMD&gt;&quot;</code> 有什么好处么？让我们来看几个场景。</p>
<h4 id="场景一让镜像变成像命令一样使用">场景一：让镜像变成像命令一样使用</h4>
<p>假设我们需要一个得知自己当前公网 IP 的镜像，那么可以先用 <code>CMD</code> 来实现：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> ubuntu:18.04</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apt-get update <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> apt-get install -y curl <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;curl&#34;</span><span class="p">,</span> <span class="s2">&#34;-s&#34;</span><span class="p">,</span> <span class="s2">&#34;https://ip.cn&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>假如我们使用 <code>docker build -t myip .</code> 来构建镜像的话，如果我们需要查询当前公网 IP，只需要执行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run myip
当前 IP：61.148.226.66 来自：北京市 联通
</code></pre></div><p>嗯，这么看起来好像可以直接把镜像当做命令使用了，不过命令总有参数，如果我们希望加参数呢？比如从上面的 <code>CMD</code> 中可以看到实质的命令是 <code>curl</code>，那么如果我们希望显示 HTTP 头信息，就需要加上 <code>-i</code> 参数。那么我们可以直接加 <code>-i</code> 参数给 <code>docker run myip</code> 么？</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run myip -i
docker: Error response from daemon: invalid header field value <span class="s2">&#34;oci runtime error: container_linux.go:247: starting container process caused \&#34;exec: \\\&#34;-i\\\&#34;: executable file not found in </span><span class="nv">$PATH</span><span class="s2">\&#34;\n&#34;</span>.
</code></pre></div><p>我们可以看到可执行文件找不到的报错，<code>executable file not found</code>。之前我们说过，跟在镜像名后面的是 <code>command</code>，运行时会替换 <code>CMD</code> 的默认值。因此这里的 <code>-i</code> 替换了原来的 <code>CMD</code>，而不是添加在原来的 <code>curl -s https://ip.cn</code> 后面。而 <code>-i</code> 根本不是命令，所以自然找不到。</p>
<p>那么如果我们希望加入 <code>-i</code> 这参数，我们就必须重新完整的输入这个命令：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run myip curl -s https://ip.cn -i
</code></pre></div><p>这显然不是很好的解决方案，而使用 <code>ENTRYPOINT</code> 就可以解决这个问题。现在我们重新用 <code>ENTRYPOINT</code> 来实现这个镜像：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> ubuntu:18.04</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apt-get update <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> apt-get install -y curl <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span class="err">
</span><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span> <span class="s2">&#34;curl&#34;</span><span class="p">,</span> <span class="s2">&#34;-s&#34;</span><span class="p">,</span> <span class="s2">&#34;https://ip.cn&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>这次我们再来尝试直接使用 <code>docker run myip -i</code>：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run myip
当前 IP：61.148.226.66 来自：北京市 联通

$ docker run myip -i
HTTP/1.1 <span class="m">200</span> OK
Server: nginx/1.8.0
Date: Tue, <span class="m">22</span> Nov <span class="m">2016</span> 05:12:40 GMT
Content-Type: text/html<span class="p">;</span> <span class="nv">charset</span><span class="o">=</span>UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

当前 IP：61.148.226.66 来自：北京市 联通
</code></pre></div><p>可以看到，这次成功了。这是因为当存在 <code>ENTRYPOINT</code> 后，<code>CMD</code> 的内容将会作为参数传给 <code>ENTRYPOINT</code>，而这里 <code>-i</code> 就是新的 <code>CMD</code>，因此会作为参数传给 <code>curl</code>，从而达到了我们预期的效果。</p>
<h4 id="场景二应用运行前的准备工作">场景二：应用运行前的准备工作</h4>
<p>启动容器就是启动主进程，但有些时候，启动主进程前，需要一些准备工作。</p>
<p>比如 <code>mysql</code> 类的数据库，可能需要一些数据库配置、初始化的工作，这些工作要在最终的 mysql 服务器运行之前解决。</p>
<p>此外，可能希望避免使用 <code>root</code> 用户去启动服务，从而提高安全性，而在启动服务前还需要以 <code>root</code> 身份执行一些必要的准备工作，最后切换到服务用户身份启动服务。或者除了服务外，其它命令依旧可以使用 <code>root</code> 身份执行，方便调试等。</p>
<p>这些准备工作是和容器 <code>CMD</code> 无关的，无论 <code>CMD</code> 为什么，都需要事先进行一个预处理的工作。这种情况下，可以写一个脚本，然后放入 <code>ENTRYPOINT</code> 中去执行，而这个脚本会将接到的参数（也就是 <code>&lt;CMD&gt;</code>）作为命令，在脚本最后执行。比如官方镜像 <code>redis</code> 中就是这么做的：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> alpine:3.4</span><span class="err">
</span><span class="err"></span>...<span class="err">
</span><span class="err"></span><span class="k">RUN</span> addgroup -S redis <span class="o">&amp;&amp;</span> adduser -S -G redis redis<span class="err">
</span><span class="err"></span>...<span class="err">
</span><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;docker-entrypoint.sh&#34;</span><span class="p">]</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 6379</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;redis-server&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>可以看到其中为了 redis 服务创建了 redis 用户，并在最后指定了 <code>ENTRYPOINT</code> 为 <code>docker-entrypoint.sh</code> 脚本。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="cp">#!/bin/sh
</span><span class="cp"></span>...
<span class="c1"># allow the container to be started with `--user`</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s1">&#39;redis-server&#39;</span> -a <span class="s2">&#34;</span><span class="k">$(</span>id -u<span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s1">&#39;0&#39;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
	chown -R redis .
	<span class="nb">exec</span> su-exec redis <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
<span class="k">fi</span>

<span class="nb">exec</span> <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span>
</code></pre></div><p>该脚本的内容就是根据 <code>CMD</code> 的内容来判断，如果是 <code>redis-server</code> 的话，则切换到 <code>redis</code> 用户身份启动服务器，否则依旧使用 <code>root</code> 身份执行。比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run -it redis id
<span class="nv">uid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span> <span class="nv">groups</span><span class="o">=</span>0<span class="o">(</span>root<span class="o">)</span>
</code></pre></div><h3 id="env-设置环境变量">ENV 设置环境变量</h3>
<p>格式有两种：</p>
<ul>
<li><code>ENV &lt;key&gt; &lt;value&gt;</code></li>
<li><code>ENV &lt;key1&gt;=&lt;value1&gt; &lt;key2&gt;=&lt;value2&gt;...</code></li>
</ul>
<p>这个指令很简单，就是设置环境变量而已，无论是后面的其它指令，如 <code>RUN</code>，还是运行时的应用，都可以直接使用这里定义的环境变量。</p>
<pre><code class="language-Dockerfie" data-lang="Dockerfie">ENV VERSION=1.0 DEBUG=on \
    NAME=&quot;Happy Feet&quot;
</code></pre><p>这个例子中演示了如何换行，以及对含有空格的值用双引号括起来的办法，这和 Shell 下的行为是一致的。</p>
<p>定义了环境变量，那么在后续的指令中，就可以使用这个环境变量。比如在官方 <code>node</code> 镜像 <code>Dockerfile</code> 中，就有类似这样的代码：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">ENV</span> NODE_VERSION 7.2.0<span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> curl -SLO <span class="s2">&#34;https://nodejs.org/dist/v</span><span class="nv">$NODE_VERSION</span><span class="s2">/node-v</span><span class="nv">$NODE_VERSION</span><span class="s2">-linux-x64.tar.xz&#34;</span> <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> curl -SLO <span class="s2">&#34;https://nodejs.org/dist/v</span><span class="nv">$NODE_VERSION</span><span class="s2">/SHASUMS256.txt.asc&#34;</span> <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> grep <span class="s2">&#34; node-v</span><span class="nv">$NODE_VERSION</span><span class="s2">-linux-x64.tar.xz\$&#34;</span> SHASUMS256.txt <span class="p">|</span> sha256sum -c - <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> tar -xJf <span class="s2">&#34;node-v</span><span class="nv">$NODE_VERSION</span><span class="s2">-linux-x64.tar.xz&#34;</span> -C /usr/local --strip-components<span class="o">=</span><span class="m">1</span> <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> rm <span class="s2">&#34;node-v</span><span class="nv">$NODE_VERSION</span><span class="s2">-linux-x64.tar.xz&#34;</span> SHASUMS256.txt.asc SHASUMS256.txt <span class="se">\
</span><span class="se"></span>  <span class="o">&amp;&amp;</span> ln -s /usr/local/bin/node /usr/local/bin/nodejs<span class="err">
</span></code></pre></div><p>在这里先定义了环境变量 <code>NODE_VERSION</code>，其后的 <code>RUN</code> 这层里，多次使用 <code>$NODE_VERSION</code> 来进行操作定制。可以看到，将来升级镜像构建版本的时候，只需要更新 <code>7.2.0</code> 即可，<code>Dockerfile</code> 构建维护变得更轻松了。</p>
<p>下列指令可以支持环境变量展开： <code>ADD</code>、<code>COPY</code>、<code>ENV</code>、<code>EXPOSE</code>、<code>LABEL</code>、<code>USER</code>、<code>WORKDIR</code>、<code>VOLUME</code>、<code>STOPSIGNAL</code>、<code>ONBUILD</code>。</p>
<p>可以从这个指令列表里感觉到，环境变量可以使用的地方很多，很强大。通过环境变量，我们可以让一份 <code>Dockerfile</code> 制作更多的镜像，只需使用不同的环境变量即可。</p>
<h3 id="arg-构建参数">ARG 构建参数</h3>
<p>格式：<code>ARG &lt;参数名&gt;[=&lt;默认值&gt;]</code></p>
<p>构建参数和 <code>ENV</code> 的效果一样，都是设置环境变量。所不同的是，<code>ARG</code> 所设置的构建环境的环境变量，在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 <code>ARG</code> 保存密码之类的信息，因为 <code>docker history</code> 还是可以看到所有值的。</p>
<p><code>Dockerfile</code> 中的 <code>ARG</code> 指令是定义参数名称，以及定义其默认值。该默认值可以在构建命令 <code>docker build</code> 中用 <code>--build-arg &lt;参数名&gt;=&lt;值&gt;</code> 来覆盖。</p>
<p>在 1.13 之前的版本，要求 <code>--build-arg</code> 中的参数名，必须在 <code>Dockerfile</code> 中用 <code>ARG</code> 定义过了，换句话说，就是 <code>--build-arg</code> 指定的参数，必须在 <code>Dockerfile</code> 中使用了。如果对应参数没有被使用，则会报错退出构建。从 1.13 开始，这种严格的限制被放开，不再报错退出，而是显示警告信息，并继续构建。这对于使用 CI 系统，用同样的构建流程构建不同的 <code>Dockerfile</code> 的时候比较有帮助，避免构建命令必须根据每个 Dockerfile 的内容修改。</p>
<h3 id="volume-定义匿名卷">VOLUME 定义匿名卷</h3>
<p>格式为：</p>
<ul>
<li><code>VOLUME [&quot;&lt;路径1&gt;&quot;, &quot;&lt;路径2&gt;&quot;...]</code></li>
<li><code>VOLUME &lt;路径&gt;</code></li>
</ul>
<p>之前我们说过，容器运行时应该尽量保持容器存储层不发生写操作，对于数据库类需要保存动态数据的应用，其数据库文件应该保存于卷(volume)中，后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷，在 <code>Dockerfile</code> 中，我们可以事先指定某些目录挂载为匿名卷，这样在运行时如果用户不指定挂载，其应用也可以正常运行，不会向容器存储层写入大量数据。</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">VOLUME</span><span class="s"> /data</span><span class="err">
</span></code></pre></div><p>这里的 <code>/data</code> 目录就会在运行时自动挂载为匿名卷，任何向 <code>/data</code> 中写入的信息都不会记录进容器存储层，从而保证了容器存储层的无状态化。当然，运行时可以覆盖这个挂载设置。比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">docker run -d -v mydata:/data xxxx
</code></pre></div><p>在这行命令中，就使用了 <code>mydata</code> 这个命名卷挂载到了 <code>/data</code> 这个位置，替代了 <code>Dockerfile</code> 中定义的匿名卷的挂载配置。</p>
<h3 id="expose-声明端口">EXPOSE 声明端口</h3>
<p>格式为 <code>EXPOSE &lt;端口1&gt; [&lt;端口2&gt;...]</code>。</p>
<p><code>EXPOSE</code> 指令是声明运行时容器提供服务端口，这只是一个声明，在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处，一个是帮助镜像使用者理解这个镜像服务的守护端口，以方便配置映射；另一个用处则是在运行时使用随机端口映射时，也就是 <code>docker run -P</code> 时，会自动随机映射 <code>EXPOSE</code> 的端口。</p>
<p>要将 <code>EXPOSE</code> 和在运行时使用 <code>-p &lt;宿主端口&gt;:&lt;容器端口&gt;</code> 区分开来。<code>-p</code>，是映射宿主端口和容器端口，换句话说，就是将容器的对应端口服务公开给外界访问，而 <code>EXPOSE</code> 仅仅是声明容器打算使用什么端口而已，并不会自动在宿主进行端口映射。</p>
<h3 id="workdir-指定工作目录">WORKDIR 指定工作目录</h3>
<p>格式为 <code>WORKDIR &lt;工作目录路径&gt;</code>。</p>
<p>使用 <code>WORKDIR</code> 指令可以来指定工作目录（或者称为当前目录），以后各层的当前目录就被改为指定的目录，如该目录不存在，<code>WORKDIR</code> 会帮你建立目录。</p>
<p>之前提到一些初学者常犯的错误是把 <code>Dockerfile</code> 等同于 Shell 脚本来书写，这种错误的理解还可能会导致出现下面这样的错误：</p>
<div class="highlight"><pre class="chroma"><code class="language-docker" data-lang="docker"><span class="k">RUN</span> <span class="nb">cd</span> /app<span class="err">
</span><span class="err"></span><span class="k">RUN</span> <span class="nb">echo</span> <span class="s2">&#34;hello&#34;</span> &gt; world.txt<span class="err">
</span></code></pre></div><p>如果将这个 <code>Dockerfile</code> 进行构建镜像运行后，会发现找不到 <code>/app/world.txt</code> 文件，或者其内容不是 <code>hello</code>。原因其实很简单，在 Shell 中，连续两行是同一个进程执行环境，因此前一个命令修改的内存状态，会直接影响后一个命令；而在 <code>Dockerfile</code> 中，这两行 <code>RUN</code> 命令的执行环境根本不同，是两个完全不同的容器。这就是对 <code>Dockerfile</code> 构建分层存储的概念不了解所导致的错误。</p>
<p>之前说过每一个 <code>RUN</code> 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 <code>RUN cd /app</code> 的执行仅仅是当前进程的工作目录变更，一个内存上的变化而已，其结果不会造成任何文件变更。而到第二层的时候，启动的是一个全新的容器，跟第一层的容器更完全没关系，自然不可能继承前一层构建过程中的内存变化。</p>
<p>因此如果需要改变以后各层的工作目录的位置，那么应该使用 <code>WORKDIR</code> 指令。</p>
<h3 id="user-指定当前用户">USER 指定当前用户</h3>
<p>格式：<code>USER &lt;用户名&gt;[:&lt;用户组&gt;]</code></p>
<p><code>USER</code> 指令和 <code>WORKDIR</code> 相似，都是改变环境状态并影响以后的层。<code>WORKDIR</code> 是改变工作目录，<code>USER</code> 则是改变之后层的执行 <code>RUN</code>, <code>CMD</code> 以及 <code>ENTRYPOINT</code> 这类命令的身份。</p>
<p>当然，和 <code>WORKDIR</code> 一样，<code>USER</code> 只是帮助你切换到指定用户而已，这个用户必须是事先建立好的，否则无法切换。</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">RUN</span> groupadd -r redis <span class="o">&amp;&amp;</span> useradd -r -g redis redis<span class="err">
</span><span class="err"></span><span class="k">USER</span><span class="s"> redis</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> <span class="o">[</span> <span class="s2">&#34;redis-server&#34;</span> <span class="o">]</span><span class="err">
</span></code></pre></div><p>如果以 <code>root</code> 执行的脚本，在执行期间希望改变身份，比如希望以某个已经建立好的用户来运行某个服务进程，不要使用 <code>su</code> 或者 <code>sudo</code>，这些都需要比较麻烦的配置，而且在 TTY 缺失的环境下经常出错。建议使用 <a href="https://github.com/tianon/gosu"><code>gosu</code></a>。</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="c"># 建立 redis 用户，并使用 gosu 换另一个用户执行命令</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> groupadd -r redis <span class="o">&amp;&amp;</span> useradd -r -g redis redis<span class="err">
</span><span class="err"></span><span class="c"># 下载 gosu</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> wget -O /usr/local/bin/gosu <span class="s2">&#34;https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64&#34;</span> <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> chmod +x /usr/local/bin/gosu <span class="se">\
</span><span class="se"></span>    <span class="o">&amp;&amp;</span> gosu nobody true<span class="err">
</span><span class="err"></span><span class="c"># 设置 CMD，并以另外的用户执行</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;exec&#34;</span><span class="p">,</span> <span class="s2">&#34;gosu&#34;</span><span class="p">,</span> <span class="s2">&#34;redis&#34;</span><span class="p">,</span> <span class="s2">&#34;redis-server&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><h3 id="healthcheck-健康检查">HEALTHCHECK 健康检查</h3>
<p>格式：</p>
<ul>
<li><code>HEALTHCHECK [选项] CMD &lt;命令&gt;</code>：设置检查容器健康状况的命令</li>
<li><code>HEALTHCHECK NONE</code>：如果基础镜像有健康检查指令，使用这行可以屏蔽掉其健康检查指令</li>
</ul>
<p><code>HEALTHCHECK</code> 指令是告诉 Docker 应该如何进行判断容器的状态是否正常，这是 Docker 1.12 引入的新指令。</p>
<p>在没有 <code>HEALTHCHECK</code> 指令前，Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题，但是如果程序进入死锁状态，或者死循环状态，应用进程并不退出，但是该容器已经无法提供服务了。在 1.12 以前，Docker 不会检测到容器的这种状态，从而不会重新调度，导致可能会有部分容器已经无法提供服务了却还在接受用户请求。</p>
<p>而自 1.12 之后，Docker 提供了 <code>HEALTHCHECK</code> 指令，通过该指令指定一行命令，用这行命令来判断容器主进程的服务状态是否还正常，从而比较真实的反应容器实际状态。</p>
<p>当在一个镜像指定了 <code>HEALTHCHECK</code> 指令后，用其启动容器，初始状态会为 <code>starting</code>，在 <code>HEALTHCHECK</code> 指令检查成功后变为 <code>healthy</code>，如果连续一定次数失败，则会变为 <code>unhealthy</code>。</p>
<p><code>HEALTHCHECK</code> 支持下列选项：</p>
<ul>
<li><code>--interval=&lt;间隔&gt;</code>：两次健康检查的间隔，默认为 30 秒；</li>
<li><code>--timeout=&lt;时长&gt;</code>：健康检查命令运行超时时间，如果超过这个时间，本次健康检查就被视为失败，默认 30 秒；</li>
<li><code>--retries=&lt;次数&gt;</code>：当连续失败指定次数后，则将容器状态视为 <code>unhealthy</code>，默认 3 次。</li>
</ul>
<p>和 <code>CMD</code>, <code>ENTRYPOINT</code> 一样，<code>HEALTHCHECK</code> 只可以出现一次，如果写了多个，只有最后一个生效。</p>
<p>在 <code>HEALTHCHECK [选项] CMD</code> 后面的命令，格式和 <code>ENTRYPOINT</code> 一样，分为 <code>shell</code> 格式，和 <code>exec</code> 格式。命令的返回值决定了该次健康检查的成功与否：<code>0</code>：成功；<code>1</code>：失败；<code>2</code>：保留，不要使用这个值。</p>
<p>假设我们有个镜像是个最简单的 Web 服务，我们希望增加健康检查来判断其 Web 服务是否在正常工作，我们可以用 <code>curl</code> 来帮助判断，其 <code>Dockerfile</code> 的 <code>HEALTHCHECK</code> 可以这么写：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> nginx</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y curl <span class="o">&amp;&amp;</span> rm -rf /var/lib/apt/lists/*<span class="err">
</span><span class="err">HEALTHCHECK --interval=5s --timeout=3s \
</span><span class="err">  </span><span class="k">CMD</span> curl -fs http://localhost/ <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span><span class="err">
</span></code></pre></div><p>这里我们设置了每 5 秒检查一次（这里为了试验所以间隔非常短，实际应该相对较长），如果健康检查命令超过 3 秒没响应就视为失败，并且使用 <code>curl -fs http://localhost/ || exit 1</code> 作为健康检查命令。</p>
<p>使用 <code>docker build</code> 来构建这个镜像：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker build -t myweb:v1 .
</code></pre></div><p>构建好了后，我们启动一个容器：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker run -d --name web -p 80:80 myweb:v1
</code></pre></div><p>当运行该镜像后，可以通过 <code>docker container ls</code> 看到最初的状态为 <code>(health: starting)</code>：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            <span class="s2">&#34;nginx -g &#39;daemon off&#34;</span>   <span class="m">3</span> seconds ago       Up <span class="m">2</span> seconds <span class="o">(</span>health: starting<span class="o">)</span>   80/tcp, 443/tcp     web
</code></pre></div><p>在等待几秒钟后，再次 <code>docker container ls</code>，就会看到健康状态变化为了 <code>(healthy)</code>：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            <span class="s2">&#34;nginx -g &#39;daemon off&#34;</span>   <span class="m">18</span> seconds ago      Up <span class="m">16</span> seconds <span class="o">(</span>healthy<span class="o">)</span>   80/tcp, 443/tcp     web
</code></pre></div><p>如果健康检查连续失败超过了重试次数，状态就会变为 <code>(unhealthy)</code>。</p>
<p>为了帮助排障，健康检查命令的输出（包括 <code>stdout</code> 以及 <code>stderr</code>）都会被存储于健康状态里，可以用 <code>docker inspect</code> 来查看。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ docker inspect --format <span class="s1">&#39;{{json .State.Health}}&#39;</span> web <span class="p">|</span> python -m json.tool
<span class="o">{</span>
    <span class="s2">&#34;FailingStreak&#34;</span>: 0,
    <span class="s2">&#34;Log&#34;</span>: <span class="o">[</span>
        <span class="o">{</span>
            <span class="s2">&#34;End&#34;</span>: <span class="s2">&#34;2016-11-25T14:35:37.940957051Z&#34;</span>,
            <span class="s2">&#34;ExitCode&#34;</span>: 0,
            <span class="s2">&#34;Output&#34;</span>: <span class="s2">&#34;&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n&lt;title&gt;Welcome to nginx!&lt;/title&gt;\n&lt;style&gt;\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n&lt;/style&gt;\n&lt;/head&gt;\n&lt;body&gt;\n&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;\n&lt;p&gt;If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.&lt;/p&gt;\n\n&lt;p&gt;For online documentation and support please refer to\n&lt;a href=\&#34;http://nginx.org/\&#34;&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;\nCommercial support is available at\n&lt;a href=\&#34;http://nginx.com/\&#34;&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;\n\n&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;\n&lt;/body&gt;\n&lt;/html&gt;\n&#34;</span>,
            <span class="s2">&#34;Start&#34;</span>: <span class="s2">&#34;2016-11-25T14:35:37.780192565Z&#34;</span>
        <span class="o">}</span>
    <span class="o">]</span>,
    <span class="s2">&#34;Status&#34;</span>: <span class="s2">&#34;healthy&#34;</span>
<span class="o">}</span>
</code></pre></div><h3 id="onbuild-为他人做嫁衣裳">ONBUILD 为他人做嫁衣裳</h3>
<p>格式：<code>ONBUILD &lt;其它指令&gt;</code>。</p>
<p><code>ONBUILD</code> 是一个特殊的指令，它后面跟的是其它指令，比如 <code>RUN</code>, <code>COPY</code> 等，而这些指令，在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像，去构建下一级镜像的时候才会被执行。</p>
<p><code>Dockerfile</code> 中的其它指令都是为了定制当前镜像而准备的，唯有 <code>ONBUILD</code> 是为了帮助别人定制自己而准备的。</p>
<p>假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 <code>npm</code> 进行包管理，所有依赖、配置、启动信息等会放到 <code>package.json</code> 文件里。在拿到程序代码后，需要先进行 <code>npm install</code> 才可以获得所有需要的依赖。然后就可以通过 <code>npm start</code> 来启动应用。因此，一般来说会这样写 <code>Dockerfile</code>：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> node:slim</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> mkdir /app<span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /app</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> ./package.json /app<span class="err">
</span><span class="err"></span><span class="k">RUN</span> <span class="o">[</span> <span class="s2">&#34;npm&#34;</span>, <span class="s2">&#34;install&#34;</span> <span class="o">]</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> . /app/<span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;npm&#34;</span><span class="p">,</span> <span class="s2">&#34;start&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>把这个 <code>Dockerfile</code> 放到 Node.js 项目的根目录，构建好镜像后，就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢？好吧，那就再把这个 <code>Dockerfile</code> 复制到第二个项目里。那如果有第三个项目呢？再复制么？文件的副本越多，版本控制就越困难，让我们继续看这样的场景维护的问题。</p>
<p>如果第一个 Node.js 项目在开发过程中，发现这个 <code>Dockerfile</code> 里存在问题，比如敲错字了、或者需要安装额外的包，然后开发人员修复了这个 <code>Dockerfile</code>，再次构建，问题解决。第一个项目没问题了，但是第二个项目呢？虽然最初 <code>Dockerfile</code> 是复制、粘贴自第一个项目的，但是并不会因为第一个项目修复了他们的 <code>Dockerfile</code>，而第二个项目的 <code>Dockerfile</code> 就会被自动修复。</p>
<p>那么我们可不可以做一个基础镜像，然后各个项目使用这个基础镜像呢？这样基础镜像更新，各个项目不用同步 <code>Dockerfile</code> 的变化，重新构建后就继承了基础镜像的更新？好吧，可以，让我们看看这样的结果。那么上面的这个 <code>Dockerfile</code> 就会变为：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> node:slim</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> mkdir /app<span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /app</span><span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;npm&#34;</span><span class="p">,</span> <span class="s2">&#34;start&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>这里我们把项目相关的构建指令拿出来，放到子项目里去。假设这个基础镜像的名字为 <code>my-node</code> 的话，各个项目内的自己的 <code>Dockerfile</code> 就变为：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> my-node</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> ./package.json /app<span class="err">
</span><span class="err"></span><span class="k">RUN</span> <span class="o">[</span> <span class="s2">&#34;npm&#34;</span>, <span class="s2">&#34;install&#34;</span> <span class="o">]</span><span class="err">
</span><span class="err"></span><span class="k">COPY</span> . /app/<span class="err">
</span></code></pre></div><p>基础镜像变化后，各个项目都用这个 <code>Dockerfile</code> 重新构建镜像，会继承基础镜像的更新。</p>
<p>那么，问题解决了么？没有。准确说，只解决了一半。如果这个 <code>Dockerfile</code> 里面有些东西需要调整呢？比如 <code>npm install</code> 都需要加一些参数，那怎么办？这一行 <code>RUN</code> 是不可能放入基础镜像的，因为涉及到了当前项目的 <code>./package.json</code>，难道又要一个个修改么？所以说，这样制作基础镜像，只解决了原来的 <code>Dockerfile</code> 的前4条指令的变化问题，而后面三条指令的变化则完全没办法处理。</p>
<p><code>ONBUILD</code> 可以解决这个问题。让我们用 <code>ONBUILD</code> 重新写一下基础镜像的 <code>Dockerfile</code>:</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> node:slim</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> mkdir /app<span class="err">
</span><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /app</span><span class="err">
</span><span class="err"></span><span class="k">ONBUILD</span> <span class="k">COPY</span> ./package.json /app<span class="err">
</span><span class="err"></span><span class="k">ONBUILD</span> <span class="k">RUN</span> <span class="o">[</span> <span class="s2">&#34;npm&#34;</span>, <span class="s2">&#34;install&#34;</span> <span class="o">]</span><span class="err">
</span><span class="err"></span><span class="k">ONBUILD</span> <span class="k">COPY</span> . /app/<span class="err">
</span><span class="err"></span><span class="k">CMD</span> <span class="p">[</span> <span class="s2">&#34;npm&#34;</span><span class="p">,</span> <span class="s2">&#34;start&#34;</span> <span class="p">]</span><span class="err">
</span></code></pre></div><p>这次我们回到原始的 <code>Dockerfile</code>，但是这次将项目相关的指令加上 <code>ONBUILD</code>，这样在构建基础镜像的时候，这三行并不会被执行。然后各个项目的 <code>Dockerfile</code> 就变成了简单地：</p>
<div class="highlight"><pre class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="k">FROM</span><span class="s"> my-node</span><span class="err">
</span></code></pre></div><p>是的，只有这么一行。当在各个项目目录中，用这个只有一行的 <code>Dockerfile</code> 构建镜像时，之前基础镜像的那三行 <code>ONBUILD</code> 就会开始执行，成功的将当前项目的代码复制进镜像、并且针对本项目执行 <code>npm install</code>，生成应用镜像。</p>
<h3 id="参考文档">参考文档</h3>
<ul>
<li>
<p><code>Dockerfie</code> 官方文档：https://docs.docker.com/engine/reference/builder/</p>
</li>
<li>
<p><code>Dockerfile</code> 最佳实践文档：https://docs.docker.com/develop/develop-images/dockerfile_best-practices/</p>
</li>
<li>
<p><code>Docker</code> 官方镜像 <code>Dockerfile</code>：https://github.com/docker-library/docs</p>
</li>
<li>
<p><code>文档</code> <a href="https://yeasy.gitbooks.io/docker_practice/image/build.html">https://yeasy.gitbooks.io/docker_practice/image/build.html</a></p>
</li>
</ul>

                
              </div>

              
                <span class="post-meta">
                  
                    #<a href="https://systemime.github.iotags/docker/">Docker</a>&nbsp;
                  
                    #<a href="https://systemime.github.iotags/dockerfile/">Dockerfile</a>&nbsp;
                  
                </span>
              

            </article>
          
        </div>
        
          <ul class="pager main-pager">
            
              <li class="previous">
                <a href="https://systemime.github.io/categories/docker/page/2">&larr; Newer Posts</a>
              </li>
            
            
              <li class="next">
                <a href="https://systemime.github.io/categories/docker/page/4">Older Posts &rarr;</a>
              </li>
            
          </ul>
        
      </div>
    <div align="center" class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        

<ul class="pagination">
    
    <li class="page-item">
        <a href="https://systemime.github.io/categories/docker/" class="page-link" aria-label="First"><span aria-hidden="true">&laquo;&laquo;</span></a>
    </li>
    
    <li class="page-item">
    <a href="https://systemime.github.io/categories/docker/page/2/" class="page-link" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a>
    </li>
    
    
    
    
    
    
    
        
        
    
    
    <li class="page-item"><a class="page-link" href="https://systemime.github.io/categories/docker/">1</a></li>
    
    
    
    
    
    
        
        
    
    
    <li class="page-item"><a class="page-link" href="https://systemime.github.io/categories/docker/page/2/">2</a></li>
    
    
    
    
    
    
        
        
    
    
    <li class="page-item active"><a class="page-link" href="https://systemime.github.io/categories/docker/page/3/">3</a></li>
    
    
    
    
    
    
        
        
    
    
    <li class="page-item"><a class="page-link" href="https://systemime.github.io/categories/docker/page/4/">4</a></li>
    
    
    <li class="page-item">
    <a href="https://systemime.github.io/categories/docker/page/4/" class="page-link" aria-label="Next"><span aria-hidden="true">&raquo;</span></a>
    </li>
    
    <li class="page-item">
        <a href="https://systemime.github.io/categories/docker/page/4/" class="page-link" aria-label="Last"><span aria-hidden="true">&raquo;&raquo;</span></a>
    </li>
    
</ul>


        
        
        
        
        
      </div>
    </div>
  </div>

    <div class="rocket"><img src="https://systemime.github.io/img/rocket_up.png" alt="" width="100" height="100"></div>
<script>
    $(function () {
      $(window).scroll(function () {
        
        if ($(window).scrollTop() >= 1000) {
          $('.rocket').stop().fadeIn(1000);
        }else {
          $('.rocket').stop().fadeOut(1000);
        }
      })
      
      $('.rocket').click(function () {
        $('html,body').stop().animate({scrollTop:0},400);
       
      })
    })

</script>

    
    <div id="landlord">
        <div class="message" style="opacity:0"></div>
        <canvas id="live2d" width="280" height="250" class="live2d"></canvas>
        <div class="hide-button">隐藏</div>
    </div>
    
    <footer id="copyright">
  <div class="container">
    <div class="row">
      <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
        <ul class="list-inline text-center footer-links">
          
          
        </ul>
        <p class="credits copyright text-muted">
        &copy;2017-2018
          
          &nbsp;&bull;&nbsp;
          September 20,2019
          updated
          
            &nbsp;&bull;&nbsp;
            <a href="https://systemime.github.io">Home</a>
          
        </p>

        <p class="credits theme-by text-muted">
        <a href="https://servicemesher.github.io" target="_blank">ServiceMesher</a>&nbsp;&bull;&nbsp;<a href="https://github.com/yangchuansheng/k8s-knowledge" target="_blank">Kubernetes 知识图谱</a>
        <br />
        <span id="busuanzi_container_site_pv">
            本站已被访问 <span id="busuanzi_value_site_pv"></span> 次啦
        </span>
        <span id="busuanzi_container_site_uv">
            &ensp;|&ensp;&thinsp;您是第 <span id="busuanzi_value_site_uv"></span> 位到访的小伙伴喔
        </span>
        </p>
        
        <p class="credits theme-by text-muted">
          <a href="http://gohugo.io">Hugo v0.67.0</a> powered &nbsp;&bull;&nbsp; Theme by <a href="http://deanattali.com/beautiful-jekyll/">Beautiful Jekyll</a> adapted to <a href="https://github.com/halogenica/beautifulhugo">Beautiful Hugo</a>
          
          <center><section class="credits theme-by text-muted">
    <span class="footer__copyright">
    <div>
    <span id="span_dt_dt"> </span>
    <script language="javascript">
      function show_date_time(){
        window.setTimeout("show_date_time()", 1000);
        BirthDay=new Date("7/8/2017 10:56:12");
        today=new Date();
        timeold=(today.getTime()-BirthDay.getTime());
        sectimeold=timeold/1000
        secondsold=Math.floor(sectimeold);
        msPerDay=24*60*60*1000
        e_daysold=timeold/msPerDay
        daysold=Math.floor(e_daysold);
        e_hrsold=(e_daysold-daysold)*24;
        hrsold=Math.floor(e_hrsold);
        e_minsold=(e_hrsold-hrsold)*60;
        minsold=Math.floor((e_hrsold-hrsold)*60);
        seconds=Math.floor((e_minsold-minsold)*60);
        span_dt_dt.innerHTML="本博客已经开心运行 "+daysold+" 天 "+hrsold+" 小时 "+minsold+" 分 "+seconds+" 秒";
      }
      show_date_time();
    </script>
    </div>
</script>
</section>
</center>
        </p>
      </div>
    </div>
  </div>
</footer>


<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/bootstrap.min.js"></script>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/photoswipe-ui-default.min.js"></script>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/photoswipe.min.js"></script>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/auto-render.min.js"></script>
<script src="https://systemime.github.io/js/main.js"></script>
<script src="https://systemime.github.io/js/clipboard.min.js"></script>
<script src="https://systemime.github.io/js/prism.js?t=123"></script>
<script src="https://systemime.github.io/js/highlight.min.js"></script>
<script src="https://res.cloudinary.com/jimmysong/raw/upload/rootsongjc-hugo/katex.min.js"></script>
<script src="https://systemime.github.io/js/reward.js"></script>
<script src="https://systemime.github.io/js/canvas_ribbon.js"></script>
<script> renderMathInElement(document.body); </script>

<script src="https://systemime.github.io/js/baguetteBox.js"></script>
<script> baguetteBox.run('.gallery');</script>






<script async defer src="https://buttons.github.io/buttons.js"></script>


<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>
<script type="text/javascript" src="https://systemime.github.io/js/quicklink.umd.js"></script>
<script>
window.addEventListener('load', () =>{
   quicklink();
});
</script>



<script type="text/javascript">
 
var a_idx = 0;
jQuery(document).ready(function($) {
    $("body").click(function(e) {
        var a = new Array("Docker", "Kubernetes", "Prometheus", "Envoy", "Istio", "Service Mesh", "Cloud Native");
        var $i = $("<span />").text(a[a_idx]);
        a_idx = (a_idx + 1) % a.length;
        var x = e.pageX,
        y = e.pageY;
        function randomColor() {
          var flakeColor = new Array("#FFDA65", "#00BFFF", "#BA55D3", "#FFA07A", "#87CEEB", "#FFB6C1");
          var snowColor = flakeColor[Math.floor(flakeColor.length * Math.random())];
          return snowColor;
        }
        $i.css({
            "z-index": 999999999999999999999999999999999999999999999999999999999999999999999,
            "top": y - 20,
            "left": x,
            "position": "absolute",
            "font-weight": "bold",
            "color": randomColor()
        });
        $("body").append($i);
        $i.animate({
            "top": y - 180,
            "opacity": 0
        },
        1500,
        function() {
            $i.remove();
        });
    });
});
</script>


<script type="text/javascript" src="https://systemime.github.io/js/lightbox.js"></script>











<script src="https://cdn.plyr.io/3.4.7/plyr.js"></script>
<script>const player = new Plyr('#player');</script>


<script async defer src="https://buttons.github.io/buttons.js"></script>


  </body>
</html>

